1. Field of the Invention
The field of the invention is data processing, or, more specifically, methods, systems, and products for write protection of subroutine return addresses.
2. Description of Related Art
Buffer overflow problems are associated with security vulnerabilities. Many security breaches have occurred due to buffer overflow. A buffer is temporary storage area in computer memory, usually in random access memory (‘RAM’). The purpose of most buffers is to act as a holding area, enabling a processor or subroutine running on a processor to manipulate data. A stack is kind of buffer in which data is removed in the reverse order from that in which it is added, so the most recently added data is the first removed. This is also called last-in, first-out (LIFO). Adding data to a stack is often referred to as ‘pushing.’ Removing data from a stack is often called ‘popping.’
A buffer may be viewed as a contiguous allocated chunk of memory, such as an array or a pointer in C. In C and C++, there are no automatic bounds checking on the buffer, which means an application or thread of execution can write past a buffer. For example:
int main ( ){ int buffer[10]; buffer[20] = 10;}
The above C program is a valid program, and every compiler can compile it without any errors. However, the program attempts to write beyond the allocated memory for the buffer, which will often result in unexpected behavior. Over the years, this concept has been used to cause tremendous damage in the computer industry.
It is first useful to consider how a process relates to computer memory. In this discussion ‘instruction stream,’ ‘thread,’ ‘thread of execution,’ and ‘process’ are used more or less as synonyms, subject to context, all of them referring to a stream of computer instructions that make up an application program. In this sense, a process or thread is a program in execution. An executable program on a disk contains a set of binary instructions to be executed by the processor; some read-only data, such as printf format strings; global and static data that lasts throughout the program execution; and a brk pointer that keeps track of the malloced memory. (This terminology is from Unix and the Intel platforms generally. The basic ideas of buffer overflow, however, are the same no matter what computer hardware platform or operating system is used.) Subroutine local variables are automatic variables created on the stack whenever functions execute, and they are cleaned up as the subroutine terminates.
FIG. 1 shows the memory layout of a process. A process image starts with the program's code and data. Code and data include the program's computer instructions and the initialized and uninitialized static and global data, respectively. After that is the run-time heap (created using malloc/calloc), and then at the top is the users stack. This stack is used whenever a subroutine call is made.
A stack is a contiguous block of memory containing data. A stack pointer points to the bottom of the stack. Whenever a subroutine call is made, the subroutine parameters are pushed onto the stack. Then the subroutine return address (address to be executed after the subroutine returns), followed by a frame pointer, is pushed on the stack. A frame pointer is used to reference the local variables and the subroutine parameters, because they are at a known distance from the frame pointer. Local automatic variables are pushed after the frame pointer. In most implementations, stacks grow from higher memory addresses to the lower ones.
FIG. 2 depicts a typical stack region as it looks when a subroutine call is being executed. Notice the frame pointer between the local and the return addresses. For this C example:
void subroutine (int a, int b, int c){ char buffer1[5]; char buffer2[10];}int main( ){ subroutine(1,2,3);},the subroutine stack looks like FIG. 3. Buffer1 takes eight bytes and buffer2 takes 12 bytes, as memory can be addressed only in multiples of word size (four bytes, in this example). In addition, a frame pointer is needed to access a, b, c, buffer1 and buffer2 variables. All these variables are cleaned up from the stack as the subroutine terminates. These variables take no space in the binary executable image of the program.
We now turn to a more detailed explanation of buffer overflow. Consider another C example:
void subroutine (char *str){ char buffer[16]; strcpy (buffer, str);}int main ( ){ char *str = “I am greater than 16 bytes”; // length of str = 27 bytes subroutine (str);}
This program is certain to cause unexpected behavior, because a string (str) of 27 bytes has been copied to a location (buffer) that has been allocated for only 16 bytes. The extra bytes run past the buffer and overwrite the space allocated for the frame pointer, return address and so on. This in turn corrupts the stack. The subroutine used to copy the string is strcpy, which completes no checking of bounds. Using strncpy would have prevented this corruption of the stack. However, this traditional example shows that a buffer overflow can overwrite a subroutine's return address, which in turn can alter the program's execution path. Recall that a subroutine's return address is the address of the next instruction in memory, which is executed immediately after the subroutine returns.
Because it is so easy to overwrite a subroutine's return address, an attacker may spawn a shell with root permissions by jumping the execution path to such code. But, what if there is no such code in the program to be exploited? The answer is to place the code that supports the attack in the buffer's overflow area. The attacker then overwrites the return address so it points back to the buffer and executes the intended code. Such code can be inserted into the program using environment variables or program input parameters.
The solutions proposed for buffer overflow problems mainly target the prevention of large-scale system attacks through the loopholes described above, although no current method can claim to prevent all possible attacks. The most often recommended method of preventing such attacks is to write secure code. Buffer overflows result from writing more code into a buffer than it is meant to hold. C library functions such as strcpy ( ), strcat ( ), sprintf ( ) and vsprintf ( ) operate on null terminated strings and perform no bounds checking. Gets ( ) is another subroutine that reads user input (into a buffer) from stdin until a terminating newline or EOF is found. The scanf ( ) family of functions also may result in buffer overflows. The most often recommended method of dealing buffer overflow problems is to not allow them to occur in the first place by minimizing the use of these vulnerable functions.
Another method of preventing buffer overflow attacks is to exclude program execution from stack memory. Because malicious code (for example, assembly instructions to spawn a root shell) is an input argument to a subroutine, it resides in the stack and not in the code segment. One way to prevent execution of such malicious code, therefore, is to invalidate the stack for execution of program instructions. Any code that attempts to execute any other code residing in the stack will cause a segmentation violation—or some other kind of interrupt. This solution is difficult to implement, however, because there are so many legitimate uses of stack execution. Some compilers, for example, use so-called ‘trampoline functions’ to take the address of a nested subroutine that relies on the stack being executable. A trampoline function is a small piece of code created at run-time when the address of a nested subroutine is taken. Such a trampoline function normally resides in the stack, that is, in the stack frame of the containing subroutine, and thus needs for the stack to be executable.
Runtime tools form the basis of other attempts to address the problem of buffer overflow attacks. Runtime tools concentrate on preventing the subroutine return address from being overwritten, because most attacks occur this way. One such tool copies the return address of a subroutine to a safe place (usually to the start of the data segment) at the start of the subroutine. When the subroutine terminates, it compares the two subroutine return addresses, the one in the stack and the one stored in data segment. Another runtime tool detects and defeats buffer overflow attacks on stacks by protecting the return address on the stack from being altered. The tool places a ‘canary’ word next to the return address whenever a subroutine is called and detects whether the canary word has been altered when the subroutine returns. Another approach is to follow the frame pointers to the correct stack frame when a buffer is passed as an argument to any of the unsafe functions. This approach then checks the distance to the nearest return address, and when the subroutine executes, it examines that address to prevent its being overwritten.
All the methods and tools described above for addressing the problem of buffer overflow attacks are limited in one manner or another. No software tool can alone solve completely the problem of buffer overflow. Code scrutiny, that is, writing secure code, is still the best overall solution to these attacks, but code scrutiny relies on the cooperation, skill, and honesty of the persons developing the software. Programmers may be tired, unskilled, and in some cases the programmer is the very attacker causing the problem. Compiler warnings may be ignored. There continues to be room for improvement regarding buffer overflow attacks.